En omfattande guide till TypeScript 'infer'-nyckelordet, som förklarar hur du anvÀnder det med villkorliga typer för kraftfull typextraktion och manipulation, inklusive avancerade anvÀndningsomrÄden.
BemÀstra TypeScript Infer: Villkorlig Typextraktion för Avancerad Typmanipulering
Typsystemet i TypeScript Àr otroligt kraftfullt, vilket gör att utvecklare kan skapa robusta och underhÄllbara applikationer. En av nyckelfunktionerna som möjliggör denna kraft Àr nyckelordet infer
som anvÀnds i kombination med villkorliga typer. Denna kombination tillhandahÄller en mekanism för att extrahera specifika typer frÄn komplexa typstrukturer. Detta blogginlÀgg fördjupar sig i infer
-nyckelordet, förklarar dess funktionalitet och visar avancerade anvÀndningsomrÄden. Vi kommer att utforska praktiska exempel som Àr tillÀmpliga pÄ olika programvaruutvecklingsscenarier, frÄn API-interaktion till komplex datastrukturmanipulering.
Vad Àr villkorliga typer?
Innan vi dyker in i infer
, lÄt oss snabbt gÄ igenom villkorliga typer. Villkorliga typer i TypeScript lÄter dig definiera en typ baserat pÄ ett villkor, liknande en ternÀr operator i JavaScript. GrundlÀggande syntaxen Àr:
T extends U ? X : Y
Detta lÀses som: "Om typen T
Ă€r tilldelningsbar till typen U
, dÄ Àr typen X
; annars Àr typen Y
."
Exempel:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Introduktion till infer
-nyckelordet
Nyckelordet infer
anvÀnds inom extends
-klausulen i en villkorlig typ för att deklarera en typvariabel som kan hÀrledas frÄn den typ som kontrolleras. I huvudsak lÄter det dig "fÄnga" en del av en typ för senare anvÀndning.
GrundlÀggande syntax:
type MyType<T> = T extends (infer U) ? U : never;
I detta exempel, om T
Àr tilldelningsbar till nÄgon typ, kommer TypeScript att försöka hÀrleda typen av U
. Om hÀrledningen lyckas, kommer typen att vara U
; annars kommer den att vara never
.
Enkla exempel pÄ infer
1. HÀrleda returtypen för en funktion
Ett vanligt anvÀndningsfall Àr att hÀrleda returtypen för en funktion:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hej, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
I detta exempel tar ReturnType<T>
en funktionstyp T
som input. Den kontrollerar om T
Àr tilldelningsbar till en funktion som accepterar vilka argument som helst och returnerar ett vÀrde. Om det Àr det, hÀrleder den returtypen som R
och returnerar den. Annars returnerar den any
.
2. HÀrleda elementtyp för en array
Ett annat anvÀndbart scenario Àr att extrahera elementtypen frÄn en array:
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
HĂ€r kontrollerar ArrayElementType<T>
om T
Àr en arraytyp. Om det Àr det, hÀrleder den elementtypen som U
och returnerar den. Om inte, returnerar den never
.
Avancerade anvÀndningsfall av infer
1. HÀrleda parametrar för en konstruktor
Du kan anvÀnda infer
för att extrahera parametertyperna för en konstruktörsfunktion:
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]
class Point {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]
I det hÀr fallet tar ConstructorParameters<T>
en konstruktörsfunktionstyp T
. Den hÀrleder typerna av konstruktörparametrarna som P
och returnerar dem som en tupel.
2. Extrahera egenskaper frÄn objekttyper
infer
kan ocksÄ anvÀndas för att extrahera specifika egenskaper frÄn objekttyper med hjÀlp av mappade typer och villkorliga typer:
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
}
type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }
type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }
//Ett grÀnssnitt som representerar geografiska koordinater.
interface GeoCoordinates {
latitude: number;
longitude: number;
altitude: number;
country: string;
city: string;
timezone: string;
}
type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }
HĂ€r skapar PickByType<T, K, U>
en ny typ som bara inkluderar egenskaperna för T
(med nycklar i K
) vars vÀrden Àr tilldelningsbara till typen U
. Den mappade typen itererar över nycklarna i T
, och den villkorliga typen filtrerar bort de nycklar som inte matchar den angivna typen.
3. Arbeta med Promises
Du kan hÀrleda den lösta typen av en Promise
:
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Data from API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
Typen Awaited<T>
tar en typ T
, som förvÀntas vara ett Promise. Typen hÀrleder sedan den lösta typen U
av Promise, och returnerar den. Om T
inte Àr ett promise, returnerar den T. Detta Àr en inbyggd hjÀlptyp i nyare versioner av TypeScript.
4. Extrahera typen av en array av Promises
Att kombinera Awaited
och arraytype-inferens gör att du kan hÀrleda den typ som löses av en array av Promises. Detta Àr sÀrskilt anvÀndbart nÀr man hanterar Promise.all
.
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
Detta exempel definierar först tvÄ asynkrona funktioner, getUSDRate
och getEURRate
, som simulerar hÀmtning av vÀxelkurser. HjÀlptypen PromiseArrayReturnType
extraherar sedan den lösta typen frÄn varje Promise
i arrayen, vilket resulterar i en tupeltyp dÀr varje element Àr den avvaktade typen av motsvarande Promise.
Praktiska exempel inom olika domÀner
1. E-handelsapplikation
TÀnk dig en e-handelsapplikation dÀr du hÀmtar produktinformation frÄn ett API. Du kan anvÀnda infer
för att extrahera typen av produktdata:
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
rating: number;
countryOfOrigin: string;
}
async function fetchProduct(productId: number): Promise<Product> {
// Simulera API-anrop
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Example Product',
price: 29.99,
description: 'En exempelprodukt',
imageUrl: 'https://example.com/image.jpg',
category: 'Elektronik',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Produktnamn: ${product.name}`);
console.log(`Pris: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
I det hÀr exemplet definierar vi ett Product
-grÀnssnitt och en fetchProduct
-funktion som hÀmtar produktinformation frÄn ett API. Vi anvÀnder Awaited
och ReturnType
för att extrahera Product
-typen frÄn fetchProduct
-funktionens returtyp, vilket gör att vi kan typkontrollera displayProductDetails
-funktionen.
2. Internationalisering (i18n)
Antag att du har en översÀttningsfunktion som returnerar olika strÀngar baserat pÄ sprÄket. Du kan anvÀnda infer
för att extrahera returtypen för den hÀr funktionen för typsÀkerhet:
interface Translations {
greeting: string;
farewell: string;
welcomeMessage: (name: string) => string;
}
const enTranslations: Translations = {
greeting: 'Hello',
farewell: 'Goodbye',
welcomeMessage: (name: string) => `Welcome, ${name}!`,
};
const frTranslations: Translations = {
greeting: 'Bonjour',
farewell: 'Au revoir',
welcomeMessage: (name: string) => `Bienvenue, ${name}!`,
};
function getTranslation(locale: 'en' | 'fr'): Translations {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.welcomeMessage(name));
}
greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!
HÀr hÀrleds TranslationType
till att vara grÀnssnittet Translations
, vilket sÀkerstÀller att funktionen greetUser
har korrekt typinformation för att komma Ät översatta strÀngar.
3. API-svarsbehandling
NĂ€r du arbetar med API:er kan svarsstrukturen vara komplex. infer
kan hjÀlpa till att extrahera specifika datatyper frÄn kapslade API-svar:
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
interface UserData {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
country: string;
language: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// Simulera API-anrop
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {
id: userId,
username: 'johndoe',
email: 'john.doe@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
country: 'USA',
language: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['data']['profile'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Namn: ${profile.firstName} ${profile.lastName}`);
console.log(`Land: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
I det hÀr exemplet definierar vi ett ApiResponse
-grÀnssnitt och ett UserData
-grÀnssnitt. Vi anvÀnder infer
och typindexering för att extrahera UserProfileType
frÄn API-svaret, vilket sÀkerstÀller att funktionen displayUserProfile
fÄr rÀtt typ.
BÀsta praxis för att anvÀnda infer
- HÄll det enkelt: AnvÀnd
infer
endast nÀr det Àr nödvÀndigt. Att överanvÀnda det kan göra din kod svÄrare att lÀsa och förstÄ. - Dokumentera dina typer: LÀgg till kommentarer för att förklara vad dina villkorliga typer och
infer
-satser gör. - Testa dina typer: AnvÀnd TypeScripts typkontroll för att sÀkerstÀlla att dina typer fungerar som förvÀntat.
- ĂvervĂ€g prestanda: Komplexa villkorliga typer kan ibland pĂ„verka kompileringstiden. Var uppmĂ€rksam pĂ„ komplexiteten i dina typer.
- AnvÀnd hjÀlptyper: TypeScript tillhandahÄller flera inbyggda hjÀlptyper (t.ex.
ReturnType
,Awaited
) som kan förenkla din kod och minska behovet av anpassadeinfer
-satser.
Vanliga fallgropar
- Felaktig inferens: Ibland kan TypeScript hÀrleda en typ som inte Àr vad du förvÀntar dig. Dubbelkolla dina typdefinitioner och villkor.
- CirkulÀra beroenden: Var försiktig nÀr du definierar rekursiva typer med
infer
, eftersom de kan leda till cirkulÀra beroenden och kompileringsfel. - Alltför komplexa typer: Undvik att skapa alltför komplexa villkorliga typer som Àr svÄra att förstÄ och underhÄlla. Bryt ner dem i mindre, mer hanterbara typer.
Alternativ till infer
Medan infer
Àr ett kraftfullt verktyg finns det situationer dÀr alternativa tillvÀgagÄngssÀtt kan vara lÀmpligare:
- TypförsÀkringar: I vissa fall kan du anvÀnda typförsÀkringar för att uttryckligen specificera typen av ett vÀrde istÀllet för att hÀrleda det. Var dock försiktig med typförsÀkringar, eftersom de kan kringgÄ typkontroll.
- Typskydd: Typskydd kan anvÀndas för att begrÀnsa typen av ett vÀrde baserat pÄ körningskontroller. Detta Àr anvÀndbart nÀr du behöver hantera olika typer baserat pÄ körningstillstÄnd.
- HjÀlptyper: TypeScript tillhandahÄller en rik uppsÀttning hjÀlptyper som kan hantera mÄnga vanliga typmanipuleringsuppgifter utan behov av anpassade
infer
-satser.
Slutsats
Nyckelordet infer
i TypeScript, nÀr det kombineras med villkorliga typer, lÄser upp avancerade typmanipuleringsmöjligheter. Det lÄter dig extrahera specifika typer frÄn komplexa typstrukturer, vilket gör att du kan skriva mer robust, underhÄllbar och typsÀker kod. FrÄn att hÀrleda funktionsreturtyper till att extrahera egenskaper frÄn objekttyper, Àr möjligheterna enorma. Genom att förstÄ principerna och bÀsta praxis som beskrivs i den hÀr guiden kan du utnyttja infer
till sin fulla potential och höja dina TypeScript-kunskaper. Kom ihÄg att dokumentera dina typer, testa dem noggrant och övervÀga alternativa tillvÀgagÄngssÀtt nÀr det Àr lÀmpligt. Att bemÀstra infer
ger dig möjlighet att skriva verkligt uttrycksfull och kraftfull TypeScript-kod, vilket i slutÀndan leder till bÀttre programvara.